/*
 * Copyright (c) 2008, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "SoapFrame.h"

// begin wxGlade: ::extracode

// end wxGlade


static void
soap_handler(struct evhttp_request *req, void *arg)
{
    ((SoapFrame*)arg)->SoapHandler(req);
}


SoapFrame::SoapFrame(struct event_base *evbase,
                     const wxString& action_name,
                     int option,
                     const struct tansu_cp_device *cp_device,
                     const struct tansu_cp_service *cp_service,
                     wxWindow* parent,
                     int id,
                     const wxString& title,
                     const wxPoint& pos,
                     const wxSize& size,
                     long style)
    : wxFrame(parent, id, title, pos, size, wxDEFAULT_FRAME_STYLE)
    , action_name(action_name)
    , option(option)
    , cp_device(cp_device)
    , cp_service(cp_service)
    , in_sizer(NULL)
    , out_sizer(NULL)
    , evbase(evbase)
    , conn(NULL)
    , req(NULL)
{
    // begin wxGlade: SoapFrame::SoapFrame
    window_1 = new wxSplitterWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3D|wxSP_BORDER);
    window_1_pane_2 = new wxScrolledWindow(window_1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
    notebook_2 = new wxNotebook(window_1_pane_2, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
    notebook_1 = new wxNotebook(window_1_pane_2, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
    window_2 = new wxSplitterWindow(window_1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_3D|wxSP_BORDER);
    window_2_pane_2 = new wxPanel(window_2, wxID_ANY);
    window_2_pane_1 = new wxPanel(window_2, wxID_ANY);
    frame_2_statusbar = CreateStatusBar(1, 0);
    label_1 = new wxStaticText(window_2_pane_1, wxID_ANY, wxT("Info"));
    list_ctrl_info = new wxListCtrl(window_2_pane_1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxSUNKEN_BORDER);
    label_2 = new wxStaticText(window_2_pane_2, wxID_ANY, wxT("Result"));
    list_ctrl_result = new wxListCtrl(window_2_pane_2, ID_SOAP_RESULT_LIST, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxSUNKEN_BORDER);
    panel_in = new wxScrolledWindow(notebook_1, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
    text_ctrl_in = new wxTextCtrl(notebook_1, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY|wxHSCROLL);
    button_exec = new wxButton(window_1_pane_2, ID_SOAP_EXECUTE, wxT("Execute"));
    button_cancel = new wxButton(window_1_pane_2, ID_SOAP_CANCEL, wxT("Cancel"));
    button_clear = new wxButton(window_1_pane_2, ID_SOAP_CLEAR, wxT("Clear"));
    button_exit = new wxButton(window_1_pane_2, ID_SOAP_EXIT, wxT("Exit"));
    panel_out = new wxScrolledWindow(notebook_2, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
    text_ctrl_out = new wxTextCtrl(notebook_2, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY|wxHSCROLL);

    set_properties();
    do_layout();
    // end wxGlade

    results.DeleteContents(true);

    if (controlURL.Len() > 0) {
        tansu_status_t status;
        char *address;
        uint16_t port;
        status = tansu_url_parse(controlURL.ToAscii(), NULL, NULL, NULL, &address, &port, NULL);
        if (status == TANSU_SUCCESS) {
            conn = evhttp_connection_base_new(evbase, NULL, address, port);
            free(address);
        }
    }
}


SoapFrame::~SoapFrame()
{
    evhttp_connection_free(conn);
}


BEGIN_EVENT_TABLE(SoapFrame, wxFrame)
    // begin wxGlade: SoapFrame::event_table
    EVT_LIST_ITEM_ACTIVATED(ID_SOAP_RESULT_LIST, SoapFrame::OnListItemActivated)
    EVT_BUTTON(ID_SOAP_EXECUTE, SoapFrame::OnExecuteButton)
    EVT_BUTTON(ID_SOAP_CANCEL, SoapFrame::OnCancelButton)
    EVT_BUTTON(ID_SOAP_CLEAR, SoapFrame::OnClearButton)
    EVT_BUTTON(ID_SOAP_EXIT, SoapFrame::OnExitButton)
    // end wxGlade
END_EVENT_TABLE();


void SoapFrame::OnListItemActivated(wxListEvent &event)
{
    event.Skip();

    long idx = event.GetIndex();
    SoapResult *result = results.Item(idx)->GetData();
    if (result == NULL)
        return;

    SetArgumentValues(*text_ctrl_in, in_labels, in_texts,
                      result->GetSoapRequest(), result->GetSoapRequestHeaders());
    SetArgumentValues(*text_ctrl_out, out_labels, out_texts,
                      result->GetSoapResponse(), result->GetSoapResponseHeaders());
}


void SoapFrame::OnExecuteButton(wxCommandEvent &event)
{
    event.Skip();

    if (conn == NULL)
        return;

    struct tdom_doc *soap = tdom_soap_new(serviceType.ToAscii(), action_name.ToAscii());
    if (soap == NULL)
        return;

    struct evhttp_request *req;
    if ((req = evhttp_request_new(soap_handler, this)) == NULL) {
        tdom_doc_delete(soap);
        return;
    }

    /* host header */
    char *address;
    uint16_t port;
    evhttp_connection_get_peer(conn, &address, &port);
    evhttp_add_header(req->output_headers, "HOST", address);

    /* other headers */
    wxString tmp = _("\"") + serviceType + _("#") + action_name + _("\"");
    evhttp_add_header(req->output_headers, "CONTENT-TYPE", "text/xml; charset=\"utf-8\"");
    evhttp_add_header(req->output_headers, "SOAPACTION", tmp.ToAscii());

    /* set soap arguments */
    int i;
    for (i = 0; i < in_labels.GetCount(); i++) {
        wxString argname = in_labels.Item(i)->GetData()->GetLabel();
        wxString value = in_texts.Item(i)->GetData()->GetValue();
        tdom_soap_set(soap, argname.ToUTF8(), value.ToUTF8());
    }

    /* serialize soap xml */
    tdom_writer_write_all(req->output_buffer, soap);

    if (evhttp_make_request(conn, req, EVHTTP_REQ_POST, controlPath.ToAscii()) != 0) {
        tdom_doc_delete(soap);
        return;
    }

    SetXML(*text_ctrl_in, soap, NULL);

    SoapResult *result = new SoapResult(soap);
    if (result == NULL)
        tdom_doc_delete(soap);

    button_exec->Enable(false);
    button_cancel->Enable(true);

    time_t t;
    time(&t);
    wxString str = UpnpFrame::GetTime(&t);

    results.Insert(result);
    long m = list_ctrl_result->InsertItem(0, str);
    list_ctrl_result->SetItemTextColour(m, *wxLIGHT_GREY);
}


void SoapFrame::OnCancelButton(wxCommandEvent &event)
{
    event.Skip();
    wxLogDebug(wxT("Event handler (SoapFrame::OnCancelButton) not implemented yet")); //notify the user that he hasn't implemented the event handler yet
}


void SoapFrame::OnClearButton(wxCommandEvent &event)
{
    event.Skip();
    Clear(in_texts);
}


void SoapFrame::OnExitButton(wxCommandEvent &event)
{
    event.Skip();
    Destroy();
}


// wxGlade: add SoapFrame event handlers


void SoapFrame::set_properties()
{
    // begin wxGlade: SoapFrame::set_properties
    SetTitle(wxT("SOAP Action"));
    SetSize(wxSize(640, 480));
    int frame_2_statusbar_widths[] = { -1 };
    frame_2_statusbar->SetStatusWidths(1, frame_2_statusbar_widths);
    const wxString frame_2_statusbar_fields[] = {
        wxEmptyString
    };
    for(int i = 0; i < frame_2_statusbar->GetFieldsCount(); ++i) {
        frame_2_statusbar->SetStatusText(frame_2_statusbar_fields[i], i);
    }
    panel_in->SetScrollRate(10, 10);
    button_cancel->Enable(false);
    panel_out->SetScrollRate(10, 10);
    window_1_pane_2->SetScrollRate(10, 10);
    // end wxGlade

    SetTitle(GetTitle() + _(" (") + action_name + _(")"));

    list_ctrl_info->InsertColumn(0, _("name"));
    list_ctrl_info->SetColumnWidth(0, 110);
    list_ctrl_info->InsertColumn(1, _("value"));
    list_ctrl_info->SetColumnWidth(1, 500);

    idx_udn = list_ctrl_info->InsertItem(0, _("UDN"));
    idx_fname = list_ctrl_info->InsertItem(1, _("friendlyName"));
    idx_dtype = list_ctrl_info->InsertItem(2, _("deviceType"));
    idx_stype = list_ctrl_info->InsertItem(3, _("serviceType"));
    idx_sid = list_ctrl_info->InsertItem(4, _("serviceId"));
    idx_url = list_ctrl_info->InsertItem(5, _("controlURL"));
    idx_action = list_ctrl_info->InsertItem(6, _("action"));

    list_ctrl_result->InsertColumn(0, _("Time"));
    list_ctrl_result->SetColumnWidth(0, 160);
    list_ctrl_result->InsertColumn(1, _("TAT"));
    list_ctrl_result->SetColumnWidth(1, 80);
    list_ctrl_result->InsertColumn(2, _("HTTP"));
    list_ctrl_result->SetColumnWidth(2, 50);
    list_ctrl_result->InsertColumn(3, _("SOAP"));
    list_ctrl_result->SetColumnWidth(3, 50);
    list_ctrl_result->InsertColumn(4, _("Error Description"));
    list_ctrl_result->SetColumnWidth(4, 300);

    SetValues();
}


void SoapFrame::do_layout()
{
    // begin wxGlade: SoapFrame::do_layout
    wxBoxSizer* sizer_1 = new wxBoxSizer(wxVERTICAL);
    wxFlexGridSizer* grid_sizer_3 = new wxFlexGridSizer(1, 3, 0, 0);
    wxFlexGridSizer* grid_sizer_4 = new wxFlexGridSizer(6, 1, 0, 0);
    wxFlexGridSizer* grid_sizer_2 = new wxFlexGridSizer(2, 1, 0, 0);
    wxFlexGridSizer* grid_sizer_1 = new wxFlexGridSizer(2, 1, 0, 0);
    grid_sizer_1->Add(label_1, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_1->Add(list_ctrl_info, 1, wxEXPAND, 0);
    window_2_pane_1->SetSizer(grid_sizer_1);
    grid_sizer_1->AddGrowableRow(1);
    grid_sizer_1->AddGrowableCol(0);
    grid_sizer_2->Add(label_2, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_2->Add(list_ctrl_result, 1, wxEXPAND, 0);
    window_2_pane_2->SetSizer(grid_sizer_2);
    grid_sizer_2->AddGrowableRow(1);
    grid_sizer_2->AddGrowableCol(0);
    window_2->SplitVertically(window_2_pane_1, window_2_pane_2);
    notebook_1->AddPage(panel_in, wxT("In"));
    notebook_1->AddPage(text_ctrl_in, wxT("Text"));
    grid_sizer_3->Add(notebook_1, 1, wxEXPAND, 0);
    grid_sizer_4->Add(20, 20, 0, wxEXPAND|wxADJUST_MINSIZE, 0);
    grid_sizer_4->Add(button_exec, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_4->Add(button_cancel, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_4->Add(button_clear, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_4->Add(20, 20, 0, wxEXPAND|wxADJUST_MINSIZE, 0);
    grid_sizer_4->Add(button_exit, 0, wxADJUST_MINSIZE, 0);
    grid_sizer_4->AddGrowableRow(0);
    grid_sizer_4->AddGrowableRow(4);
    grid_sizer_4->AddGrowableCol(0);
    grid_sizer_3->Add(grid_sizer_4, 1, wxEXPAND, 0);
    notebook_2->AddPage(panel_out, wxT("Out"));
    notebook_2->AddPage(text_ctrl_out, wxT("Text"));
    grid_sizer_3->Add(notebook_2, 1, wxEXPAND, 0);
    window_1_pane_2->SetSizer(grid_sizer_3);
    grid_sizer_3->AddGrowableRow(0);
    grid_sizer_3->AddGrowableCol(0);
    grid_sizer_3->AddGrowableCol(2);
    window_1->SplitHorizontally(window_2, window_1_pane_2, 200);
    sizer_1->Add(window_1, 1, wxEXPAND, 0);
    SetSizer(sizer_1);
    Layout();
    // end wxGlade
}


void SoapFrame::SetValues()
{
    if (cp_device == NULL || cp_service == NULL)
        return;
    if (cp_device->dd == NULL || cp_service->scpd == NULL)
        return;

    const char *tmp;
    struct tdom_elem *elem;

    wxString udn(cp_device->udn, wxConvUTF8);
    list_ctrl_info->SetItem(idx_udn, 1, udn);

    struct tdom_elem *device;
    device = cp_device->embedded ? cp_device->embedded : cp_device->rootdevice;
    if (device == NULL)
        return;

    tmp = tansu_dd_get_elem_value(device, "deviceType");
    if (tmp)
        list_ctrl_info->SetItem(idx_dtype, 1, wxString::FromUTF8(tmp));

    tmp = tansu_dd_get_elem_value(device, "friendlyName");
    if (tmp)
        list_ctrl_info->SetItem(idx_fname, 1, wxString::FromUTF8(tmp));

    serviceType = wxString::FromUTF8(cp_service->type);
    list_ctrl_info->SetItem(idx_stype, 1, serviceType);
    list_ctrl_info->SetItem(idx_sid, 1, wxString::FromUTF8(cp_service->sid));

    const char *base = tansu_dd_get_elem_value(cp_device->dd->root, "URLBase");

    elem = tansu_dd_get_service(device, cp_service->sid);
    tmp = tansu_dd_get_elem_value(tansu_dd_get_service(device, cp_service->sid), "controlURL");
    if (tmp) {
        char *abs = tansu_url_get_abs(cp_device->location, base, tmp);
        if (abs) {
            controlURL = wxString::FromAscii(abs);
            list_ctrl_info->SetItem(idx_url, 1, controlURL);
            if (tansu_url_parse(abs, NULL, NULL, NULL, NULL, NULL, &tmp) == TANSU_SUCCESS)
                controlPath = wxString::FromAscii(tmp);
            free(abs);
        }
    }

    list_ctrl_info->SetItem(idx_action, 1, action_name);

    struct tdom_elem *action;
    action = tansu_scpd_get_action(cp_service->scpd, action_name.ToUTF8());
    if (action == NULL)
        return;

    SetArguments(action);
}

void SoapFrame::SetArguments(struct tdom_elem *action)
{
    struct tdom_elem *argumentList = tansu_scpd_get_elem(action, "argumentList", 0);
    if (argumentList == NULL)
        return;

    int n = tdom_count_elem(argumentList, "argument", TANSU_NS_UPNP_SERVICE);
    int i;

    for (i = 0; i < n; i++) {
        const char *tmp;
        const char *name;
        int inarg = 1;
        long style = wxTE_MULTILINE|wxHSCROLL;
        wxPanel *panel = panel_in;
        LabelList *labels = &in_labels;;
        TextList *texts = &in_texts;
        
        struct tdom_elem *argument = tansu_scpd_get_elem(argumentList, "argument", i);

        if ((name = tansu_scpd_get_elem_value(argument, "name")) == NULL)
            continue;
        if ((tmp = tansu_scpd_get_elem_value(argument, "direction")) == NULL)
            continue;

        if (strcmp(tmp, "out") == 0) {
            inarg = 0;
            panel = panel_out;
            style |= wxTE_READONLY;
            labels = &out_labels;
            texts = &out_texts;
        } else if (strcmp(tmp, "in") != 0) {
            continue;
        }

        wxStaticText *label;
        wxTextCtrl *text_ctrl;
        label = new wxStaticText(panel, wxID_ANY, wxString::FromUTF8(name));
        text_ctrl = new wxTextCtrl(panel, wxID_ANY, wxEmptyString,
                                   wxDefaultPosition, wxDefaultSize, style);
        labels->Append(label);
        texts->Append(text_ctrl);
    }

    in_sizer = LayoutArguments(in_labels, in_texts);
    panel_in->SetSizer(in_sizer);
    out_sizer = LayoutArguments(out_labels, out_texts);
    panel_out->SetSizer(out_sizer);
}

void SoapFrame::SetArgumentValues(wxTextCtrl& text_ctrl,
                                  LabelList& labels,
                                  TextList& texts,
                                  struct tdom_doc *doc,
                                  struct evkeyvalq *headers)
{
    Clear(texts);
    text_ctrl.SetValue(wxEmptyString);

    int i;
    for (i = 0; i < labels.GetCount(); i++) {
        wxString str = labels.Item(i)->GetData()->GetLabel();
        const char *tmp = tdom_soap_get(doc, str.ToUTF8());
        wxTextCtrl *text = texts.Item(i)->GetData();
        if (text)
            SetTextValue(*text, tmp);
    }

    SetXML(text_ctrl, doc, headers);
}

wxFlexGridSizer *
SoapFrame::LayoutArguments(LabelList& labels, TextList& texts)
{
    int i;
    int n = labels.GetCount();

    wxFlexGridSizer* sizer = new wxFlexGridSizer(n, 1, 0, 0);

    for (i = 0; i < n; i++) {
        wxControl *label = labels.Item(i)->GetData();
        wxControl *text = texts.Item(i)->GetData();
        sizer->Add(label, 0, wxADJUST_MINSIZE, 0);
        sizer->Add(text, 0, wxEXPAND|wxADJUST_MINSIZE, 0);
        sizer->AddGrowableRow((i * 2)+ 1);
    }
    sizer->AddGrowableCol(0);

    return sizer;
}

void SoapFrame::Clear(TextList& texts)
{
    int i;
    for (i = 0; i < texts.GetCount(); i++) {
        texts.Item(i)->GetData()->SetValue(wxEmptyString);
    }
}

void SoapFrame::SoapHandler(struct evhttp_request *req)
{
    struct timeval tv;
    tansu_time_get_monotonic(&tv);

    button_exec->Enable(true);
    button_cancel->Enable(false);

    wxString tmp;
    int n = 0;
    int status_code = -1;
    const wxColour *color = wxBLACK;
    SoapResult *result = results.Item(0)->GetData();
    wxString readable = wxEmptyString;
    struct tdom_doc *soap = NULL;

    if (req == NULL) {
        frame_2_statusbar->SetStatusText(_("action failed"));
        color = wxRED;
        goto out;
    }

    tmp = text_ctrl_in->GetValue();
    SetXML(*text_ctrl_in, NULL, req->output_headers);
    text_ctrl_in->SetValue(text_ctrl_in->GetValue() + tmp);

    status_code = req->response_code;
    if (req->response_code_line)
        readable = wxString::FromAscii(req->response_code_line);

    if (req->response_code == HTTP_OK) {
        soap = tansu_xml_parse_evbuffer(req->input_buffer);
        if (soap)
            SetArgumentValues(*text_ctrl_out, out_labels, out_texts, soap, req->input_headers);
    } else if (req->response_code == 500) {
        soap = tansu_xml_parse_evbuffer(req->input_buffer);
        if (soap) {
            struct tdom_elem *elem;
            elem = tansu_soap_get_error(soap);
            const char *p;
            if ((p = tdom_get_value(tdom_get_elem(elem, "errorCode", NULL, 0))))
                list_ctrl_result->SetItem(n, 3, wxString::FromUTF8(p));
            if ((p = tdom_get_value(tdom_get_elem(elem, "errorDescription", NULL, 0))))
                readable = wxString::FromUTF8(p);
            SetXML(*text_ctrl_out, soap, req->input_headers);
        }
    } else {
        tmp = wxString::Format(_("action failed %d"), req->response_code);
        frame_2_statusbar->SetStatusText(tmp);
    }

    if (result) {
        result->SetRequestHeaders(req->output_headers);
        result->SetResult(soap, req->input_headers);
    } else {
        tdom_doc_delete(soap);
    }

out:
    if (result) {
        tansu_time_sub(&tv, result->GetTimestamp());
        int min = tv.tv_sec / 60 % 60;
        int sec = tv.tv_sec % 60;
        int msec = tv.tv_usec / 1000;
        tmp = wxString::Format(_("%02d:%02d.%03d"), min, sec, msec);
        list_ctrl_result->SetItem(n, 1, tmp);
    }
    list_ctrl_result->SetItem(n, 2, wxString::Format(_("%d"), status_code));
    list_ctrl_result->SetItem(n, 4, readable);
    list_ctrl_result->SetItemTextColour(n, *color);
}

void SoapFrame::SetXML(wxTextCtrl& text_ctrl, struct tdom_doc *doc, struct evkeyvalq *headers)
{
    text_ctrl.SetValue(wxEmptyString);

    struct evbuffer *buffer = evbuffer_new();
    if (buffer == NULL)
        return;

    if (headers) {
        struct evkeyval *kv;
        TAILQ_FOREACH(kv, headers, next) {
            evbuffer_add_printf(buffer, "%s: %s\n", kv->key, kv->value);
        }
        evbuffer_add_printf(buffer, "\n");
    }

    if (doc)
        tdom_writer_write_all(buffer, doc);

    text_ctrl.SetValue(wxString::FromUTF8((char*)EVBUFFER_DATA(buffer)));
    evbuffer_free(buffer);
}

void SoapFrame::SetTextValue(wxTextCtrl& text_ctrl, const char *value)
{
    if (value == NULL) {
        text_ctrl.SetValue(wxEmptyString);
        return;
    }

    if (value[0] == '<') {
        struct tdom_doc *doc = tansu_xml_parse(value, strlen(value));
        if (doc) {
            SetXML(text_ctrl, doc);
            tdom_doc_delete(doc);
            return;
        }
    }

    text_ctrl.SetValue(wxString::FromUTF8(value));
}

#include <wx/listimpl.cpp>
WX_DEFINE_LIST(LabelList);
WX_DEFINE_LIST(TextList);
WX_DEFINE_LIST(SoapResultList);

